/*
 * Copyright (c) 1998, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     04/01/2011-2.3 Guy Pelletier
//       - 337323: Multi-tenant with shared schema support (part 2)
//     09/09/2011-2.3.1 Guy Pelletier
//       - 356197: Add new VPD type to MultitenantType
package org.eclipse.persistence.queries;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor;
import org.eclipse.persistence.internal.databaseaccess.DatabaseCall;
import org.eclipse.persistence.internal.databaseaccess.DatabasePlatform;
import org.eclipse.persistence.internal.descriptors.ObjectBuilder;
import org.eclipse.persistence.internal.expressions.QueryKeyExpression;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.internal.helper.InvalidObject;
import org.eclipse.persistence.internal.helper.ThreadCursoredList;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.ResultSetRecord;
import org.eclipse.persistence.internal.sessions.SimpleResultSetRecord;
import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl;
import org.eclipse.persistence.internal.sessions.remote.RemoteSessionController;
import org.eclipse.persistence.internal.sessions.remote.Transporter;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.OneToManyMapping;
import org.eclipse.persistence.sessions.DatabaseRecord;
import org.eclipse.persistence.sessions.SessionProfiler;
import org.eclipse.persistence.sessions.remote.DistributedSession;
import org.eclipse.persistence.tools.profiler.QueryMonitor;

/**
 * <p><b>Purpose</b>:
 * Concrete class for all read queries involving a collection of objects.
 *
 * <p><b>Responsibilities</b>:
 * Return a container of the objects generated by the query.
 * Implements the inheritance feature when dealing with abstract descriptors
 *
 * @author Yvon Lavoie
 * @since TOPLink/Java 1.0
 */
public class ReadAllQuery extends ObjectLevelReadQuery {
    /** Used for collection and stream support. */
    protected ContainerPolicy containerPolicy;

    /** Used for Oracle HierarchicalQuery support */
    protected Expression startWithExpression;
    protected Expression connectByExpression;
    protected List<Expression> orderSiblingsByExpressions;
    protected Direction direction;

    /**
     * Specifies the direction in which the hierarchy is traversed in a
     * hierarchical query.
     */
    public static enum Direction {
        /**
         * Hierarchy will be traversed from parent to child - PRIOR keyword is
         * generated on the left side of the equation
         */
        PARENT_TO_CHILD,
        /**
         * Hierarchy will be traversed from child to parent - PRIOR keyword is
         * generated on the right side of the equation
         */
        CHILD_TO_PARENT;

        /**
         * PUBLIC: Returns the default hierarchy traversal direction for the
         * specified mapping.<br>
         * For OneToOne mappings, source in parent object goes to target in
         * child object, collections are the opposite way.
         *
         * @param mapping
         *            The mapping for which to return default hierarchy
         *            traversal direction
         * @return The default hierarchy traversal direction for the mapping
         *         passed
         */
        public static Direction getDefault(DatabaseMapping mapping) {
            return mapping != null && mapping.isOneToOneMapping() ? CHILD_TO_PARENT : PARENT_TO_CHILD;
        }
    }

    /**
     * PUBLIC:
     * Return a new read all query.
     * A reference class must be specified before execution.
     * It is better to provide the class and expression builder on construction to ensure a single expression builder is used.
     * If no selection criteria is specified this will read all objects of the class from the database.
     */
    public ReadAllQuery() {
        super();
        setContainerPolicy(ContainerPolicy.buildDefaultPolicy());
    }

    /**
     * PUBLIC:
     * Return a new read all query.
     * It is better to provide the class and expression builder on construction to ensure a single expression builder is used.
     * If no selection criteria is specified this will read all objects of the class from the database.
     */
    public ReadAllQuery(Class classToRead) {
        this();
        this.referenceClass = classToRead;
    }

    /**
     * PUBLIC:
     * Return a new read all query for the class and the selection criteria.
     */
    public ReadAllQuery(Class classToRead, Expression selectionCriteria) {
        this();
        this.referenceClass = classToRead;
        setSelectionCriteria(selectionCriteria);
    }

    /**
     * PUBLIC:
     * Return a new read all query for the class.
     * The expression builder must be used for all associated expressions used with the query.
     */
    public ReadAllQuery(Class classToRead, ExpressionBuilder builder) {
        this();
        this.defaultBuilder = builder;
        this.referenceClass = classToRead;
    }

    /**
     * PUBLIC:
     * Return a new read all query.
     * The call represents a database interaction such as SQL, Stored Procedure.
     */
    public ReadAllQuery(Class classToRead, Call call) {
        this();
        this.referenceClass = classToRead;
        setCall(call);
    }

    /**
     * PUBLIC:
     * Return a query by example query to find all objects matching the attributes of the example object.
     */
    public ReadAllQuery(Object exampleObject, QueryByExamplePolicy policy) {
        this();
        setExampleObject(exampleObject);
        setQueryByExamplePolicy(policy);
    }

    /**
     * PUBLIC:
     * The expression builder should be provide on creation to ensure only one is used.
     */
    public ReadAllQuery(ExpressionBuilder builder) {
        this();
        this.defaultBuilder = builder;
    }

    /**
     * PUBLIC:
     * Create a read all query with the database call.
     */
    public ReadAllQuery(Call call) {
        this();
        setCall(call);
    }

    /**
     * PUBLIC:
     * Order the query results by the object's attribute or query key name.
     */
    public void addAscendingOrdering(String queryKeyName) {
        addOrdering(getExpressionBuilder().get(queryKeyName).ascending());
    }

    /**
     * INTERNAL:
     * <P> This method is called by the object builder when building an original.
     * It will cause the original to be cached in the query results if the query
     * is set to do so.
     */
    @Override
    public void cacheResult(Object unwrappedOriginal) {
        Collection container = (Collection)getTemporaryCachedQueryResults();
        if (container == null) {
            container = (Collection)getContainerPolicy().containerInstance();
            setTemporaryCachedQueryResults(container);
        }
        getContainerPolicy().addInto(unwrappedOriginal, container, getSession());
    }

    /**
     * INTERNAL:
     * The cache check is done before the prepare as a hit will not require the work to be done.
     */
    @Override
    protected Object checkEarlyReturnLocal(AbstractSession session, AbstractRecord translationRow) {
        // Check for in-memory only query.
        if (shouldCheckCacheOnly()) {
            // assert !isReportQuery();
            if (shouldUseWrapperPolicy()) {
                getContainerPolicy().setElementDescriptor(this.descriptor);
            }

            // PERF: Fixed to not query each unit of work cache (is not conforming),
            // avoid hashtable and primary key indexing.
            // At some point we may need to support some kind of in-memory with conforming option,
            // but we do not currently allow this.
            AbstractSession rootSession = session;
            while (rootSession.isUnitOfWork()) {
                rootSession = rootSession.getParent();
            }
            Vector allCachedVector = rootSession.getIdentityMapAccessor().getAllFromIdentityMap(getSelectionCriteria(), getReferenceClass(), translationRow, getInMemoryQueryIndirectionPolicyState(), false);

            // Must ensure that all of the objects returned are correctly registered in the unit of work.
            if (session.isUnitOfWork()) {
                allCachedVector = ((UnitOfWorkImpl)session).registerAllObjects(allCachedVector);
            }

            this.isCacheCheckComplete = true;

            return getContainerPolicy().buildContainerFromVector(allCachedVector, session);
        } else {
            return null;
        }
    }

    /**
     * INTERNAL:
     * Check and return custom query flag. Custom query flag value is initialized when stored value is {@code null}.
     * Called from {@link #checkForCustomQuery(AbstractSession, AbstractRecord)} to retrieve custom query flag.
     * @param session        Current session (not used).
     * @param translationRow Database record (not used).
     * @return Current custom query flag. Value will never be {@code null}.
     */
    @Override
    protected Boolean checkCustomQueryFlag(final AbstractSession session, final AbstractRecord translationRow) {
        // #436871 - Use local copy to avoid NPE from concurrent modification.
        final Boolean useCustomQuery = isCustomQueryUsed;
        if (useCustomQuery != null) {
            return useCustomQuery;
        // Initialize custom query flag.
        } else {
            final boolean useCustomQueryValue =
                    !isUserDefined() && isExpressionQuery() && getSelectionCriteria() == null
                    && isDefaultPropertiesQuery() && (!hasOrderByExpressions())
                    && descriptor.getQueryManager().hasReadAllQuery();
            setIsCustomQueryUsed(useCustomQueryValue);
            return Boolean.valueOf(useCustomQueryValue);
        }
    }

    /**
     * INTERNAL:
     * Get custom all read query from query manager.
     * Called from {@link #checkForCustomQuery(AbstractSession, AbstractRecord)} to retrieve custom read query.
     * @return Custom all read query from query manager.
     */
    @Override
    protected ObjectLevelReadQuery getReadQuery() {
        return descriptor.getQueryManager().getReadAllQuery();
    }

    /**
     * INTERNAL:
     * Creates and returns a copy of this query.
     * @return A clone of this instance.
     */
    @Override
    public Object clone() {
        final ReadAllQuery cloneQuery = (ReadAllQuery)super.clone();
        // Don't use setters as that will trigger unprepare.
        cloneQuery.containerPolicy = containerPolicy.clone(cloneQuery);
        return cloneQuery;
    }

    /**
     * INTERNAL:
     * Conform the result if specified.
     */
    protected Object conformResult(Object result, UnitOfWorkImpl unitOfWork, AbstractRecord arguments, boolean buildDirectlyFromRows) {
        Expression selectionCriteria = getSelectionCriteria();
        if (selectionCriteria != null) {
            ExpressionBuilder builder = getSelectionCriteria().getBuilder();
            builder.setSession(unitOfWork.getRootSession(null));
            builder.setQueryClass(getReferenceClass());
            if (getQueryMechanism().isExpressionQueryMechanism() && selectionCriteria.isLogicalExpression()) {
                // bug #526546
                if (builder.derivedExpressions != null) {
                    for (Expression e : builder.derivedExpressions) {
                        if (e.isQueryKeyExpression() && ((QueryKeyExpression) e).shouldQueryToManyRelationship()) {
                            DatabaseMapping mapping = ((QueryKeyExpression) e).getMapping();
                            if (mapping.isOneToManyMapping()) {
                                OneToManyMapping otm = (OneToManyMapping) mapping;
                                Expression join = otm.buildSelectionCriteria();
                                selectionCriteria = selectionCriteria.and(join);
                            }
                        }
                    }
                }
            }

        }

        // If the query is redirected then the collection returned might no longer
        // correspond to the original container policy.  CR#2342-S.M.
        ContainerPolicy cp;
        if (getRedirector() != null) {
            cp = ContainerPolicy.buildPolicyFor(result.getClass());
        } else {
            cp = getContainerPolicy();
        }

        // This code is now a great deal different...  For one, registration is done
        // as part of conforming.  Also, this should only be called if one actually
        // is conforming.
        // First scan the UnitOfWork for conforming instances.
        // This will walk through the entire cache of registered objects.
        // Let p be objects from result not in the cache.
        // Let c be objects from cache.
        // Presently p intersect c = empty set, but later p subset c.
        // By checking cache now doesConform will be called p fewer times.
        Map<Object, Object> indexedInterimResult = unitOfWork.scanForConformingInstances(selectionCriteria, getReferenceClass(), arguments, this);

        Cursor cursor = null;
        // In the case of cursors just conform/register the initially read collection.
        if (cp.isCursorPolicy()) {
            cursor = (Cursor)result;
            cp = ContainerPolicy.buildPolicyFor(ClassConstants.Vector_class);
            // In nested UnitOfWork session might have been session of the parent.
            cursor.setSession(unitOfWork);
            result = cursor.getObjectCollection();
            // for later incremental conforming...
            cursor.setInitiallyConformingIndex(indexedInterimResult);
            cursor.setSelectionCriteriaClone(getSelectionCriteria());
            cursor.setTranslationRow(arguments);
        }

        // Now conform the result from the database.
        // Remove any deleted or changed objects that no longer conform.
        // Deletes will only work for simple queries, queries with or's or anyof's may not return
        // correct results when untriggered indirection is in the model.
        List fromDatabase = null;

        // When building directly from rows, one of the performance benefits
        // is that we no longer have to wrap and then unwrap the originals.
        // result is just a vector, not a container of wrapped originals.
        if (buildDirectlyFromRows) {
            List<AbstractRecord> rows = (List<AbstractRecord>)result;
            int size = rows.size();
            fromDatabase = new ArrayList(size);
            for (int index = 0; index < size; index++) {
                AbstractRecord row = rows.get(index);
                // null is placed in the row collection for 1-m joining to filter duplicate rows.
                if (row != null) {
                    Object clone = conformIndividualResult(buildObject(row), unitOfWork, arguments, getSelectionCriteria(), indexedInterimResult);
                    if (clone != null) {
                        fromDatabase.add(clone);
                    }
                }
            }
        } else {
            fromDatabase = new ArrayList(cp.sizeFor(result));
            AbstractSession sessionToUse = unitOfWork.getParent();
            for (Object iter = cp.iteratorFor(result); cp.hasNext(iter);) {
                Object object = cp.next(iter, sessionToUse);
                Object clone = conformIndividualResult(registerIndividualResult(object, null, unitOfWork, null, null), unitOfWork, arguments, getSelectionCriteria(), indexedInterimResult);
                if (clone != null) {
                    fromDatabase.add(clone);
                }
            }
        }

        // Now add the unwrapped conforming instances into an appropriate container.
        // Wrapping is done automatically.
        // Make sure a vector of exactly the right size is returned.
        Object conformedResult = cp.containerInstance(indexedInterimResult.size() + fromDatabase.size());
        for (Iterator enumtr = indexedInterimResult.values().iterator(); enumtr.hasNext();) {
            Object eachClone = enumtr.next();
            cp.addInto(eachClone, conformedResult, unitOfWork);
        }
        int size = fromDatabase.size();
        for (int index = 0; index < size; index++) {
            Object eachClone = fromDatabase.get(index);
            cp.addInto(eachClone, conformedResult, unitOfWork);
        }

        if (cursor != null) {
            cursor.setObjectCollection((List)conformedResult);

            // For nested UOW must copy all in object collection to
            // initiallyConformingIndex, as some of these could have been from
            // the parent UnitOfWork.
            if (unitOfWork.isNestedUnitOfWork()) {
                for (Object clone : cursor.getObjectCollection()) {
                    indexedInterimResult.put(clone, clone);
                }
            }
            return cursor;
        } else {
            return conformedResult;
        }
    }

    /**
     * INTERNAL:
     * Execute the query. If there are cached results return those.
     * This must override the super to support result caching.
     *
     * @param session - the session in which the receiver will be executed.
     * @return An object or vector, the result of executing the query.
     * @exception DatabaseException - an error has occurred on the database
     */
    @Override
    public Object execute(AbstractSession session, AbstractRecord row) throws DatabaseException {
        if (shouldCacheQueryResults()) {
            if (getContainerPolicy().overridesRead()) {
                throw QueryException.cannotCacheCursorResultsOnQuery(this);
            }
            if (shouldConformResultsInUnitOfWork()) {
                throw QueryException.cannotConformAndCacheQueryResults(this);
            }
            if (isPrepared()) {// only prepared queries can have cached results.
                Object queryResults = getQueryResults(session, row, true);
                if (queryResults != null) {
                    if (QueryMonitor.shouldMonitor()) {
                        QueryMonitor.incrementReadAllHits(this);
                    }
                    session.incrementProfile(SessionProfiler.CacheHits, this);
                    // bug6138532 - check for "cached no results" (InvalidObject singleton) in query
                    // results, and return an empty container instance as configured
                    if (queryResults == InvalidObject.instance) {
                        return getContainerPolicy().containerInstance(0);
                    }
                    Collection results = (Collection)queryResults;
                    if (session.isUnitOfWork()) {
                        ContainerPolicy policy = getContainerPolicy();
                        Object resultCollection = policy.containerInstance(results.size());
                        Object iterator = policy.iteratorFor(results);
                        while (policy.hasNext(iterator)) {
                            Object result = ((UnitOfWorkImpl)session).registerExistingObject(policy.next(iterator, session), this.descriptor, null, true);
                            policy.addInto(result, resultCollection, session);
                        }
                        return resultCollection;
                    }
                    return results;
                }
            }
            session.incrementProfile(SessionProfiler.CacheMisses, this);
        }
        if (QueryMonitor.shouldMonitor()) {
            QueryMonitor.incrementReadAllMisses(this);
        }
        return super.execute(session, row);
    }

    /**
     * INTERNAL:
     * Execute the query.
     * Get the rows and build the object from the rows.
     * @exception  DatabaseException - an error has occurred on the database
     * @return java.lang.Object collection of objects resulting from execution of query.
     */
    @Override
    protected Object executeObjectLevelReadQuery() throws DatabaseException {
        Object result = null;

        if (this.containerPolicy.overridesRead()) {
            this.executionTime = System.currentTimeMillis();
            return this.containerPolicy.execute();
        }

        if (this.descriptor.isDescriptorForInterface()) {
            Object returnValue = this.descriptor.getInterfacePolicy().selectAllObjectsUsingMultipleTableSubclassRead(this);
            this.executionTime = System.currentTimeMillis();
            return returnValue;
        }

        if (this.descriptor.hasTablePerClassPolicy() && this.descriptor.isAbstract()) {
            result = this.containerPolicy.containerInstance();

            if (this.shouldIncludeData) {
                ComplexQueryResult complexResult = new ComplexQueryResult();
                complexResult.setResult(result);
                complexResult.setData(new ArrayList());
                result = complexResult;
            }
        } else {
            Object sopObject = getTranslationRow().getSopObject();
            boolean useOptimization = false;
            if (sopObject == null) {
                useOptimization = usesResultSetAccessOptimization();
            }

            if (useOptimization) {
                DatabaseCall call = ((DatasourceCallQueryMechanism)this.queryMechanism).selectResultSet();
                this.executionTime = System.currentTimeMillis();
                Statement statement = call.getStatement();
                ResultSet resultSet = call.getResult();
                DatabaseAccessor dbAccessor = (DatabaseAccessor)getAccessor();
                boolean exceptionOccured = false;
                try {
                    if (this.session.isUnitOfWork()) {
                        result = registerResultSetInUnitOfWork(resultSet, call.getFields(), call.getFieldsArray(), (UnitOfWorkImpl)this.session, this.translationRow);
                    } else {
                        result = this.containerPolicy.containerInstance();
                        this.descriptor.getObjectBuilder().buildObjectsFromResultSetInto(this, resultSet, call.getFields(), call.getFieldsArray(), result);
                    }
                } catch (SQLException exception) {
                    exceptionOccured = true;
                    DatabaseException commException = dbAccessor.processExceptionForCommError(this.session, exception, call);
                    if (commException != null) {
                        throw commException;
                    }
                    throw DatabaseException.sqlException(exception, call, dbAccessor, this.session, false);
                } finally {
                    try {
                        if (resultSet != null) {
                            resultSet.close();
                        }
                        if (dbAccessor != null) {
                            if (statement != null) {
                                dbAccessor.releaseStatement(statement, call.getSQLString(), call, this.session);
                            }
                        }
                        if (call.hasAllocatedConnection()) {
                            getExecutionSession().releaseConnectionAfterCall(this);
                        }
                    } catch (RuntimeException cleanupException) {
                        if (!exceptionOccured) {
                            throw cleanupException;
                        }
                    } catch (SQLException cleanupSQLException) {
                        if (!exceptionOccured) {
                            throw DatabaseException.sqlException(cleanupSQLException, call, dbAccessor, this.session, false);
                        }
                    }
                }
            } else {
                List<AbstractRecord> rows;
                if (sopObject != null) {
                    Object valuesIterator = this.containerPolicy.iteratorFor(getTranslationRow().getSopObject());
                    int size = this.containerPolicy.sizeFor(sopObject);
                    rows =  new ArrayList<>(size);
                    while (this.containerPolicy.hasNext(valuesIterator)) {
                        Object memberSopObject = this.containerPolicy.next(valuesIterator, this.session);
                        DatabaseRecord memberRow = new DatabaseRecord(0);
                        memberRow.setSopObject(memberSopObject);
                        rows.add(memberRow);
                    }
                    this.executionTime = System.currentTimeMillis();
                } else {
                    rows = getQueryMechanism().selectAllRows();
                    this.executionTime = System.currentTimeMillis();

                    // If using 1-m joins, must set all rows.
                    if (hasJoining() && this.joinedAttributeManager.isToManyJoin()) {
                        this.joinedAttributeManager.setDataResults(rows, this.session);
                    }
                    // Batch fetching in IN requires access to the rows to build the id array.
                    if ((this.batchFetchPolicy != null) && this.batchFetchPolicy.isIN()) {
                        this.batchFetchPolicy.setDataResults(rows);
                    }
                }

                if (this.session.isUnitOfWork()) {
                    result = registerResultInUnitOfWork(rows, (UnitOfWorkImpl)this.session, this.translationRow, true);//
                } else {
                    if (rows instanceof ThreadCursoredList) {
                        result = this.containerPolicy.containerInstance();
                    } else {
                        result = this.containerPolicy.containerInstance(rows.size());
                    }
                    this.descriptor.getObjectBuilder().buildObjectsInto(this, rows, result);
                }

                if (sopObject != null) {
                    if (!this.descriptor.getObjectBuilder().isSimple()) {
                        // remove sopObject so it's not stuck in any value holder.
                        for (AbstractRecord row : rows) {
                            row.setSopObject(null);
                        }
                    }
                } else {
                    if (this.shouldIncludeData) {
                        ComplexQueryResult complexResult = new ComplexQueryResult();
                        complexResult.setResult(result);
                        complexResult.setData(rows);
                        result = complexResult;
                    }
                }
            }
        }

        // Add the other (already registered) results and return them.
        if (this.descriptor.hasTablePerClassPolicy()) {
            result = this.containerPolicy.concatenateContainers(
                    result, this.descriptor.getTablePerClassPolicy().selectAllObjectsUsingMultipleTableSubclassRead(this), this.session);
        }

        // If the results were empty, then ensure they get cached still.
        if (shouldCacheQueryResults() && this.containerPolicy.isEmpty(result)) {
            this.temporaryCachedQueryResults = InvalidObject.instance();
        }

        return result;
    }

    /**
     * INTERNAL:
     * Execute the query building the objects directly from the database result-set.
     * @exception  DatabaseException - an error has occurred on the database
     * @return an ArrayList of the resulting objects.
     */
    @Override
    protected Object executeObjectLevelReadQueryFromResultSet() throws DatabaseException {
        AbstractSession session = this.session;
        DatabasePlatform platform = session.getPlatform();
        DatabaseCall call = ((DatasourceCallQueryMechanism)this.queryMechanism).selectResultSet();
        Statement statement = call.getStatement();
        ResultSet resultSet = call.getResult();
        DatabaseAccessor accessor = (DatabaseAccessor)getAccessor();
        boolean exceptionOccured = false;
        try {
            ResultSetMetaData metaData = resultSet.getMetaData();
            List results = new ArrayList();
            ObjectBuilder builder = this.descriptor.getObjectBuilder();
            while (resultSet.next()) {
                results.add(builder.buildObjectFromResultSet(this, this.joinedAttributeManager, resultSet, session, accessor, metaData, platform, call.getFields(), call.getFieldsArray()));
            }
            return results;
        } catch (SQLException exception) {
            exceptionOccured = true;
            DatabaseException commException = accessor.processExceptionForCommError(session, exception, call);
            if (commException != null) {
                throw commException;
            }
            throw DatabaseException.sqlException(exception, call, accessor, session, false);
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    accessor.releaseStatement(statement, call.getSQLString(), call, session);
                }
                if (accessor != null) {
                    session.releaseReadConnection(accessor);
                }
            } catch (SQLException exception) {
                if (!exceptionOccured) {
                    //in the case of an external connection pool the connection may be null after the statement release
                    // if it is null we will be unable to check the connection for a comm error and
                    //therefore must return as if it was not a comm error.
                    DatabaseException commException = accessor.processExceptionForCommError(session, exception, call);
                    if (commException != null) {
                        throw commException;
                    }
                    throw DatabaseException.sqlException(exception, call, accessor, session, false);
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Extract the correct query result from the transporter.
     */
    @Override
    public Object extractRemoteResult(Transporter transporter) {
        return ((DistributedSession)getSession()).getObjectsCorrespondingToAll(transporter.getObject(), transporter.getObjectDescriptors(), new IdentityHashMap(), this, getContainerPolicy());
    }

    /**
     * INTERNAL:
     * Return the query's container policy.
     */
    public ContainerPolicy getContainerPolicy() {
        return containerPolicy;
    }

    /**
     * INTERNAL:
     * Returns the specific default redirector for this query type.  There are numerous default query redirectors.
     * See ClassDescriptor for their types.
     */
    @Override
    protected QueryRedirector getDefaultRedirector() {
        return descriptor.getDefaultReadAllQueryRedirector();
    }

    /**
     * PUBLIC:
     * @return Expression - the start with expression used to generated the hierarchical query clause in
     * Oracle
     */
    public Expression getStartWithExpression() {
        return startWithExpression;
    }

    /**
     * PUBLIC:
     * @return Expression - the connect by expression used to generate the hierarchical query caluse in Oracle
     */
    public Expression getConnectByExpression() {
        return connectByExpression;
    }

    /**
     * PUBLIC:
     * @return {@literal List<Expression>} - the ordering expressions used to generate the hierarchical query clause in Oracle
     */
    public List<Expression> getOrderSiblingsByExpressions() {
        return orderSiblingsByExpressions;
    }

    /**
     * PUBLIC:
     * @return Direction - the direction in which the hierarchy is traversed
     */
    public Direction getDirection() {
        return direction;
    }

    /**
     * INTERNAL:
     * Verify that we have hierarchical query expressions
     */
    public boolean hasHierarchicalExpressions() {
        return ((this.startWithExpression != null) || (this.connectByExpression != null) || (this.orderSiblingsByExpressions != null));
    }

    /**
     * INTERNAL:
     * Return true if the query uses default properties.
     * This is used to determine if this query is cacheable.
     * i.e. does not use any properties that may conflict with another query
     * with the same JPQL or selection criteria.
     */
    @Override
    public boolean isDefaultPropertiesQuery() {
        return super.isDefaultPropertiesQuery()
            && (!hasBatchReadAttributes())
            && (!hasHierarchicalExpressions())
            && (!this.containerPolicy.isCursorPolicy());
    }

    /**
     * INTERNAL:
     * Return if the query is equal to the other.
     * This is used to allow dynamic expression query SQL to be cached.
     */
    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!super.equals(object)) {
            return false;
        }
        ReadAllQuery query = (ReadAllQuery) object;
        if (!this.containerPolicy.equals(query.containerPolicy)) {
            return false;
        }
        return true;
    }

    /**
     * PUBLIC:
     * Return if this is a read all query.
     */
    @Override
    public boolean isReadAllQuery() {
        return true;
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    @Override
    protected void prepare() throws QueryException {
        if ((!isReportQuery()) && prepareFromCachedQuery()) {
            return;
        }
        super.prepare();

        this.containerPolicy.prepare(this, getSession());

        if (hasJoining() && isExpressionQuery()) {
            // 1-m join fetching with pagination requires an order by.
            if (this.joinedAttributeManager.isToManyJoin()
                    && ((this.maxRows > 0) || (this.firstResult > 0) || this.containerPolicy.isCursorPolicy())) {
                if (!hasOrderByExpressions()) {
                    for (DatabaseField primaryKey : this.descriptor.getPrimaryKeyFields()) {
                        addOrdering(getExpressionBuilder().getField(primaryKey));
                    }
                }
            }
        }

        if (this.containerPolicy.overridesRead()) {
            return;
        }

        if (this.descriptor.isDescriptorForInterface()) {
            return;
        }

        prepareSelectAllRows();

        if (!isReportQuery()) {
            // should be called after prepareSelectRow so that the call knows whether it returns ResultSet
            prepareResultSetAccessOptimization();
        }
    }

    /**
     * INTERNAL:
     * Prepare the query from the prepared query.
     * This allows a dynamic query to prepare itself directly from a prepared query instance.
     * This is used in the JPQL parse cache to allow preparsed queries to be used to prepare
     * dynamic queries.
     * This only copies over properties that are configured through JPQL.
     */
    @Override
    public void prepareFromQuery(DatabaseQuery query) {
        super.prepareFromQuery(query);
        if (query.isReadAllQuery()) {
            ReadAllQuery readQuery = (ReadAllQuery)query;
            this.containerPolicy = readQuery.containerPolicy;
            if (readQuery.hasHierarchicalExpressions()) {
                this.orderSiblingsByExpressions = readQuery.orderSiblingsByExpressions;
                this.connectByExpression = readQuery.connectByExpression;
                this.startWithExpression = readQuery.startWithExpression;
            }
        }
    }

    /**
     * INTERNAL:
     * Set the properties needed to be cascaded into the custom query.
     */
    @Override
    protected void prepareCustomQuery(DatabaseQuery customQuery) {
        super.prepareCustomQuery(customQuery);
        ReadAllQuery customReadQuery = (ReadAllQuery)customQuery;
        customReadQuery.containerPolicy = this.containerPolicy;
        customReadQuery.cascadePolicy = this.cascadePolicy;
        customReadQuery.shouldRefreshIdentityMapResult = this.shouldRefreshIdentityMapResult;
        customReadQuery.shouldMaintainCache = this.shouldMaintainCache;
        customReadQuery.shouldUseWrapperPolicy = this.shouldUseWrapperPolicy;
    }

    /**
     * INTERNAL:
     * Prepare the receiver for execution in a session.
     */
    @Override
    public void prepareForExecution() throws QueryException {
        super.prepareForExecution();

        this.containerPolicy.prepareForExecution();

        // Modifying the translation row here will modify it on the original
        // query which is not good. So we have to clone the translation row if
        // we are going to append tenant discriminator fields to it.
        if (descriptor.hasMultitenantPolicy()) {
            translationRow = translationRow.clone();
            descriptor.getMultitenantPolicy().addFieldsToRow(translationRow, getSession());
        }
    }

    /**
     * INTERNAL:
     * Prepare the mechanism.
     */
    protected void prepareSelectAllRows() {
        getQueryMechanism().prepareSelectAllRows();
    }

    /**
     * INTERNAL:
     * All objects queried via a UnitOfWork get registered here.  If the query
     * went to the database.
     * <p>
     * Involves registering the query result individually and in totality, and
     * hence refreshing / conforming is done here.
     *
     * @param result may be collection (read all) or an object (read one),
     * or even a cursor.  If in transaction the shared cache will
     * be bypassed, meaning the result may not be originals from the parent
     * but raw database rows.
     * @param unitOfWork the unitOfWork the result is being registered in.
     * @param arguments the original arguments/parameters passed to the query
     * execution.  Used by conforming
     * @param buildDirectlyFromRows If in transaction must construct
     * a registered result from raw database rows.
     *
     * @return the final (conformed, refreshed, wrapped) UnitOfWork query result
     */
    @Override
    public Object registerResultInUnitOfWork(Object result, UnitOfWorkImpl unitOfWork, AbstractRecord arguments, boolean buildDirectlyFromRows) {
        // For bug 2612366: Conforming results in UOW extremely slow.
        // Replacing results with registered versions in the UOW is a part of
        // conforming and is now done while conforming to maximize performance.
        if (unitOfWork.hasCloneMapping() // PERF: Avoid conforming empty uow.
                    && (shouldConformResultsInUnitOfWork() || this.descriptor.shouldAlwaysConformResultsInUnitOfWork())) {
            return conformResult(result, unitOfWork, arguments, buildDirectlyFromRows);
        }

        // When building directly from rows, one of the performance benefits
        // is that we no longer have to wrap and then unwrap the originals.
        // result is just a vector, not a collection of wrapped originals.
        // Also for cursors the initial connection is automatically registered.
        if (buildDirectlyFromRows) {
            List<AbstractRecord> rows = (List<AbstractRecord>)result;
            ContainerPolicy cp = this.containerPolicy;
            int size = rows.size();
            Object clones = cp.containerInstance(size);
            if(cp.shouldAddAll()) {
                List clonesIn = new ArrayList(size);
                List<AbstractRecord> rowsIn = new ArrayList(size);
                for (int index = 0; index < size; index++) {
                    AbstractRecord row = rows.get(index);

                    // null is placed in the row collection for 1-m joining to filter duplicate rows.
                    if (row != null) {
                        Object clone = buildObject(row);
                        clonesIn.add(clone);
                        rowsIn.add(row);
                    }
                }
                cp.addAll(clonesIn, clones, unitOfWork, rowsIn, this, null, true);
            } else {
                boolean quickAdd = (clones instanceof Collection) && !this.descriptor.getObjectBuilder().hasWrapperPolicy();
                if (this.descriptor.getCachePolicy().shouldPrefetchCacheKeys()
                        && shouldMaintainCache()
                        && ! shouldRetrieveBypassCache()
                        && ((!(unitOfWork.hasCommitManager() && unitOfWork.getCommitManager().isActive())
                                && ! unitOfWork.wasTransactionBegunPrematurely()
                                && !this.descriptor.getCachePolicy().shouldIsolateObjectsInUnitOfWork()
                                && ! this.descriptor.getCachePolicy().shouldIsolateProtectedObjectsInUnitOfWork())
                                || (unitOfWork.isClassReadOnly(this.getReferenceClass(), this.getDescriptor())))){
                    Object[] pkList = new Object[size];
                    for (int i = 0; i< size; ++i){
                        pkList[i] = getDescriptor().getObjectBuilder().extractPrimaryKeyFromRow(rows.get(i), session);
                    }
                    setPrefetchedCacheKeys(unitOfWork.getParentIdentityMapSession(this).getIdentityMapAccessorInstance().getAllCacheKeysFromIdentityMapWithEntityPK(pkList, descriptor));
                }
                for (int index = 0; index < size; index++) {
                    AbstractRecord row = rows.get(index);

                    // null is placed in the row collection for 1-m joining to filter duplicate rows.
                    if (row != null) {
                        Object clone = buildObject(row);
                        if (quickAdd) {
                            ((Collection)clones).add(clone);
                        } else {
                            cp.addInto(clone, clones, unitOfWork, row, this, null, true);
                        }
                    }
                }
            }
            return clones;
        }

        ContainerPolicy cp;
        Cursor cursor = null;

        // If the query is redirected then the collection returned might no longer
        // correspond to the original container policy.  CR#2342-S.M.
        if (getRedirector() != null) {
            cp = ContainerPolicy.buildPolicyFor(result.getClass());
        } else {
            cp = this.containerPolicy;
        }

        // In the case of cursors just register the initially read collection.
        if (cp.isCursorPolicy()) {
            cursor = (Cursor)result;
            // In nested UnitOfWork session might have been session of the parent.
            cursor.setSession(unitOfWork);
            cp = ContainerPolicy.buildPolicyFor(ClassConstants.Vector_class);
            result = cursor.getObjectCollection();
        }

        Object clones = cp.containerInstance(cp.sizeFor(result));
        AbstractSession sessionToUse = unitOfWork.getParent();
        for (Object iter = cp.iteratorFor(result); cp.hasNext(iter);) {
            Object object = cp.next(iter, sessionToUse);
            Object clone = registerIndividualResult(object, null, unitOfWork, this.joinedAttributeManager, null);
            cp.addInto(clone, clones, unitOfWork);
        }
        if (cursor != null) {
            cursor.setObjectCollection((Vector)clones);
            return cursor;
        } else {
            return clones;
        }
    }

    /**
     * INTERNAL:
     * Version of the previous method for ResultSet optimization.
     *
     * @return the final (conformed, refreshed, wrapped) UnitOfWork query result
     */
    public Object registerResultSetInUnitOfWork(ResultSet resultSet, Vector fields, DatabaseField[] fieldsArray, UnitOfWorkImpl unitOfWork, AbstractRecord arguments) throws SQLException {
        // TODO: add support for Conforming results in UOW - currently conforming in uow is not compatible with ResultSet optimization.

        ContainerPolicy cp = this.containerPolicy;
        Object clones = cp.containerInstance();
        ResultSetMetaData metaData = resultSet.getMetaData();
        boolean hasNext = resultSet.next();
        if (hasNext) {
            // TODO: possibly add support for SortedListContainerPolicy (cp.shouldAddAll() == true) - this policy currently is not compatible with ResultSet optimization
            boolean quickAdd = (clones instanceof Collection) && !this.descriptor.getObjectBuilder().hasWrapperPolicy();
            DatabaseAccessor dbAccessor = (DatabaseAccessor)getAccessor();
            boolean useSimple = this.descriptor.getObjectBuilder().isSimple();
            AbstractSession executionSession = getExecutionSession();
            DatabasePlatform platform = dbAccessor.getPlatform();
            boolean optimizeData = platform.shouldOptimizeDataConversion();
            if (useSimple) {
                // None of the fields are relational - the row could be reused, just clear all the values.
                SimpleResultSetRecord row = new SimpleResultSetRecord(fields, fieldsArray, resultSet, metaData, dbAccessor, executionSession, platform, optimizeData);
                if (this.descriptor.isDescriptorTypeAggregate()) {
                    // Aggregate Collection may have an unmapped primary key referencing the owner, the corresponding field will not be used when the object is populated and therefore may not be cleared.
                    row.setShouldKeepValues(true);
                }
                while (hasNext) {
                    Object clone = buildObject(row);
                    if (quickAdd) {
                        ((Collection)clones).add(clone);
                    } else {
                        // TODO: investigate is it possible to support MappedKeyMapPolicy - this policy currently is not compatible with ResultSet optimization
                        cp.addInto(clone, clones, unitOfWork);
                    }
                    row.reset();
                    hasNext = resultSet.next();
                }
            } else {
                boolean shouldKeepRow = this.descriptor.getObjectBuilder().shouldKeepRow();
                while (hasNext) {
                    ResultSetRecord row = new ResultSetRecord(fields, fieldsArray, resultSet, metaData, dbAccessor, executionSession, platform, optimizeData);
                    Object clone = buildObject(row);
                    if (quickAdd) {
                        ((Collection)clones).add(clone);
                    } else {
                        // TODO: investigate is it possible to support MappedKeyMapPolicy - this policy currently is not compatible with ResultSet optimization
                        cp.addInto(clone, clones, unitOfWork);
                    }

                    if (shouldKeepRow) {
                        if (row.hasResultSet()) {
                            // ResultSet has not been fully triggered - that means the cached object was used.
                            // Yet the row still may be cached in a value holder (see loadBatchReadAttributes and loadJoinedAttributes methods).
                            // Remove ResultSet to avoid attempt to trigger it (already closed) when pk or fk values (already extracted) accessed when the value holder is instantiated.
                            row.removeResultSet();
                        } else {
                            row.removeNonIndirectionValues();
                        }
                    }
                    hasNext = resultSet.next();
                }
            }
        }
        return clones;
    }

    /**
     * INTERNAL:
     * Execute the query through remote session.
     */
    @Override
    public Object remoteExecute() {
        if (this.containerPolicy.overridesRead()) {
            return this.containerPolicy.remoteExecute();
        }

        Object cacheHit = checkEarlyReturn(this.session, this.translationRow);
        if (cacheHit != null) {
            return cacheHit;
        }

        return super.remoteExecute();
    }

    /**
     * INTERNAL:
     * replace the value holders in the specified result object(s)
     */
    @Override
    public Map replaceValueHoldersIn(Object object, RemoteSessionController controller) {
        return controller.replaceValueHoldersInAll(object, getContainerPolicy());
    }

    /**
     * PUBLIC:
     * Set the container policy. Used to support different containers
     * (e.g. Collections, Maps).
     */
    public void setContainerPolicy(ContainerPolicy containerPolicy) {
        // CR#... a container policy is always required, default is vector,
        // required for deployment XML.
        if (containerPolicy == null) {
            return;
        }
        this.containerPolicy = containerPolicy;
        setIsPrepared(false);
    }

    /**
     * PUBLIC:
     * Set the Hierarchical Query Clause for the query
     * <p>Example:
     * <p>Expression startWith = builder.get("id").equal(new Integer(100)); //can be any expression which identifies a set of employees
     * <p>Expression connectBy = builder.get("managedEmployees"); //indicated the relationship that the hierarchy is based on, must be self-referential
     * <p>Vector orderBy = new Vector();
     * <p>orderBy.addElement(builder.get("startDate"));
     * <p>readAllQuery.setHierarchicalQueryClause(startWith, connectBy, orderBy);
     *
     * <p>This query would generate SQL like this:
     * <p>SELECT * FROM EMPLOYEE START WITH ID=100 CONNECT BY PRIOR ID = MANAGER_ID ORDER SIBLINGS BY START_DATE
     *
     * @param startWith Describes the START WITH clause of the query - null if not needed
     * @param connectBy This should be a query key expression which indicates an attribute who's mapping describes the hierarchy
     * @param orderSiblingsExpressions Contains expressions which indicate fields to be included in the ORDER SIBLINGS BY clause - null if not required
     */
    public void setHierarchicalQueryClause(Expression startWith, Expression connectBy, List<Expression> orderSiblingsExpressions) {
        setHierarchicalQueryClause(startWith, connectBy, orderSiblingsExpressions, null);
    }

    /**
     * PUBLIC: Set the Hierarchical Query Clause for the query, specifying the
     * hierarchy traversal direction
     * <p>
     * Example:
     * <p>
     * Expression startWith = builder.get("id").equal(new Integer(100)); //can
     * be any expression which identifies a set of employees <br>
     * Expression connectBy = builder.get("managedEmployees"); //indicated the
     * relationship that the hierarchy is based on, must be self-referential <br>
     * Vector orderBy = new Vector(); <br>
     * orderBy.addElement(builder.get("startDate")); <br>
     * readAllQuery.setHierarchicalQueryClause(startWith, connectBy, orderBy,
     * Direction.CHILD_TO_PARENT);
     *
     * <p>
     * This query would generate SQL like this:
     * <p>
     * SELECT * FROM EMPLOYEE START WITH ID=100 CONNECT BY ID = PRIOR MANAGER_ID
     * ORDER SIBLINGS BY START_DATE
     *
     * @param startWith
     *            Describes the START WITH clause of the query - null if not
     *            needed
     * @param connectBy
     *            This should be a query key expression which indicates an
     *            attribute who's mapping describes the hierarchy
     * @param orderSiblingsExpressions
     *            Contains expressions which indicate fields to be included in
     *            the ORDER SIBLINGS BY clause - null if not required
     * @param direction
     *            The direction in which the hierarchy is traversed; if not
     *            specified, CHILD_TO_PARENT is used for OneToOne relationships
     *            and PARENT_TO_CHILD is used for collections
     */
    public void setHierarchicalQueryClause(Expression startWith, Expression connectBy, List<Expression> orderSiblingsExpressions, Direction direction) {
        this.startWithExpression = startWith;
        this.connectByExpression = connectBy;
        this.orderSiblingsByExpressions = orderSiblingsExpressions;
        this.direction = direction;
        setIsPrepared(false);
    }

    /**
     * PUBLIC:
     * Configure the mapping to use an instance of the specified container class
     * to hold the target objects.
     * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Collection interface.
     * <p>jdk1.1.x: The container class must be a subclass of Vector.
     */
    public void useCollectionClass(Class concreteClass) {
        // Set container policy.
        setContainerPolicy(ContainerPolicy.buildPolicyFor(concreteClass));

    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * The initial read size is 10 and page size is 5.
     */
    public void useCursoredStream() {
        useCursoredStream(10, 5);
    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * @param initialReadSize the initial number of objects to read
     * @param pageSize the number of objects to read when more objects
     * are needed from the database
     */
    public void useCursoredStream(int initialReadSize, int pageSize) {
        setContainerPolicy(new CursoredStreamPolicy(this, initialReadSize, pageSize));
    }

    /**
     * PUBLIC:
     * Use a CursoredStream as the result collection.
     * @param initialReadSize the initial number of objects to read
     * @param pageSize the number of objects to read when more objects
     * are needed from the database
     * @param sizeQuery a query that will return the size of the result set;
     * this must be set if an expression is not used (i.e. custom SQL)
     */
    public void useCursoredStream(int initialReadSize, int pageSize, ValueReadQuery sizeQuery) {
        setContainerPolicy(new CursoredStreamPolicy(this, initialReadSize, pageSize, sizeQuery));
    }

    /**
     * PUBLIC:
     * Configure the query to use an instance of the specified container class
     * to hold the result objects. The key used to index the value in the Map
     * is the value returned by a call to the specified zero-argument method.
     * The method must be implemented by the class (or a superclass) of the
     * value to be inserted into the Map.
     * <p>jdk1.2.x: The container class must implement (directly or indirectly) the Map interface.
     * <p>jdk1.1.x: The container class must be a subclass of Hashtable.
     * <p>The referenceClass must set before calling this method.
     */
    public void useMapClass(Class concreteClass, String methodName) {
        // the reference class has to be specified before coming here
        if (getReferenceClass() == null) {
            throw QueryException.referenceClassMissing(this);
        }
        ContainerPolicy policy = ContainerPolicy.buildPolicyFor(concreteClass);
        policy.setKeyName(methodName, getReferenceClass().getName());
        setContainerPolicy(policy);
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     */
    public void useScrollableCursor() {
        useScrollableCursor(10);
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     * @param pageSize the number of elements to be read into a the cursor
     * when more elements are needed from the database.
     */
    public void useScrollableCursor(int pageSize) {
        setContainerPolicy(new ScrollableCursorPolicy(this, pageSize));
    }

    /**
     * PUBLIC:
     * Use a ScrollableCursor as the result collection.
     * @param policy the scrollable cursor policy allows for additional result set options.
     * Example:<p>
     * ScrollableCursorPolicy policy = new ScrollableCursorPolicy()<p>
     * policy.setResultSetType(ScrollableCursorPolicy.TYPE_SCROLL_INSENSITIVE);<p>
     * query.useScrollableCursor(policy);
     */
    public void useScrollableCursor(ScrollableCursorPolicy policy) {
        policy.setQuery(this);
        setContainerPolicy(policy);
    }

    /**
     * INTERNAL:
     * Indicates whether the query can use ResultSet optimization.
     * The method is called when the query is prepared,
     * so it should refer only to the attributes that cannot be altered without re-preparing the query.
     * If the query is a clone and the original has been already prepared
     * this method will be called to set a (transient and therefore set to null) usesResultSetOptimization attribute.
     */
    @Override
    public boolean supportsResultSetAccessOptimizationOnPrepare() {
        if (!super.supportsResultSetAccessOptimizationOnPrepare()) {
            return false;
        }
        return !this.containerPolicy.isMappedKeyMapPolicy() && !this.containerPolicy.shouldAddAll() &&  // MappedKeyMapPolicy requires the whole row, OrderListContainerPolicy requires all rows.
                !this.descriptor.shouldAlwaysConformResultsInUnitOfWork();  // will be supported when conformResult method is adapted to use ResultSet;
    }

    /**
     * INTERNAL:
     * Indicates whether the query can use ResultSet optimization.
     * Note that the session must be already set.
     * The method is called when the query is executed,
     * so it should refer only to the attributes that can be altered without re-preparing the query.
     */
    @Override
    public boolean supportsResultSetAccessOptimizationOnExecute() {
        if (!super.supportsResultSetAccessOptimizationOnExecute()) {
            return false;
        }
        return !shouldConformResultsInUnitOfWork(); // could be supported if conformResult method is adapted to use ResultSetAccessOptimization
    }
}
